package com.baselet.gwt.client.element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.baselet.control.StringStyle;
import com.baselet.control.basics.geom.DimensionDouble;
import com.baselet.control.basics.geom.PointDouble;
import com.baselet.control.enums.AlignHorizontal;
import com.baselet.control.enums.FormatLabels;
import com.baselet.control.enums.LineType;
import com.baselet.diagram.draw.DrawFunction;
import com.baselet.diagram.draw.DrawHandler;
import com.baselet.diagram.draw.helper.ColorOwn;
import com.baselet.diagram.draw.helper.Style;
import com.baselet.gwt.client.base.Converter;
import com.baselet.gwt.client.base.Notification;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.Context2d.TextAlign;
public class DrawHandlerGwt extends DrawHandler {
private static final Logger log = LoggerFactory.getLogger(DrawHandlerGwt.class);
private final Canvas canvas;
private final Context2d ctx;
public DrawHandlerGwt(Canvas canvas) {
this.canvas = canvas;
ctx = canvas.getContext2d();
}
@Override
protected DimensionDouble textDimensionHelper(StringStyle singleLine) {
String oldFont = ctx.getFont();
ctxSetFont(style.getFontSize(), singleLine);
DimensionDouble dim = new DimensionDouble(ctx.measureText(singleLine.getStringWithoutMarkup()).getWidth(), style.getFontSize()); // unfortunately a html canvas offers no method to get the exakt height, therefore just use the fontsize
ctx.setFont(oldFont); // restore old font to make sure the textDimensions method doesnt change context state!
return dim;
}
@Override
protected double getDefaultFontSize() {
return 12;
}
@Override
public void drawArc(final double x, final double y, final double width, final double height, final double start, final double extent, final boolean open) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
double centerX = (int) (x + width / 2) + HALF_PX;
double centerY = (int) (y + height / 2) + HALF_PX;
ctx.save();
// translate the arc and don't use the center parameters because they are affected by scaling
ctx.translate(centerX, centerY);
ctx.scale(1, height / width);
if (open) { // if arc should be open, move before the path begins
ctx.beginPath();
}
else { // otherwise the move is part of the path
ctx.beginPath();
ctx.moveTo(0, 0);
}
ctx.arc(0, 0, width / 2, -Math.toRadians(start), -Math.toRadians(start + extent), true);
if (!open) { // close path only if arc is not open
ctx.closePath();
}
// restore before drawing so the line has the same with and is not affected by the scaling
ctx.restore();
ctx.fill();
if (styleAtDrawingCall.getLineWidth() > 0) {
ctx.stroke();
}
}
});
}
@Override
public void drawCircle(final double x, final double y, final double radius) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
ctx.beginPath();
ctx.arc((int) x + HALF_PX, (int) y + HALF_PX, radius, 0, 2 * Math.PI);
ctx.fill();
if (styleAtDrawingCall.getLineWidth() > 0) {
ctx.stroke();
}
}
});
}
@Override
public void drawEllipse(final double x, final double y, final double width, final double height) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
drawEllipseHelper(ctx, styleAtDrawingCall.getLineWidth() > 0, (int) x + HALF_PX, (int) y + HALF_PX, width, height);
}
});
}
@Override
public void drawLines(final PointDouble... points) {
if (points.length > 1) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
drawLineHelper(styleAtDrawingCall.getLineWidth() > 0, points);
}
});
}
}
@Override
public void drawRectangle(final double x, final double y, final double width, final double height) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
// int cast on x/y + HALF_PX and int cast on width/height is important to make sure it never draws between pixels
ctx.fillRect((int) x + HALF_PX, (int) y + HALF_PX, (int) width, (int) height);
ctx.beginPath();
ctx.rect((int) x + HALF_PX, (int) y + HALF_PX, (int) width, (int) height);
if (styleAtDrawingCall.getLineWidth() > 0) {
ctx.stroke();
}
}
});
}
@Override
public void drawRectangleRound(final double x, final double y, final double width, final double height, final double radius) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
setStyle(ctx, styleAtDrawingCall);
drawRoundRectHelper(ctx, styleAtDrawingCall.getLineWidth() > 0, (int) x + HALF_PX, (int) y + HALF_PX, (int) width, (int) height, radius);
}
});
}
@Override
public void printHelper(final StringStyle[] text, final PointDouble point, final AlignHorizontal align) {
final Style styleAtDrawingCall = style.cloneFromMe();
addDrawable(new DrawFunction() {
@Override
public void run() {
PointDouble pToDraw = point;
ColorOwn fgColor = getOverlay().getForegroundColor() != null ? getOverlay().getForegroundColor() : styleAtDrawingCall.getForegroundColor();
ctx.setFillStyle(Converter.convert(fgColor));
for (StringStyle line : text) {
drawTextHelper(line, pToDraw, align, styleAtDrawingCall.getFontSize());
pToDraw = new PointDouble(pToDraw.getX(), pToDraw.getY() + textHeightMax());
}
}
});
}
private void drawTextHelper(final StringStyle line, PointDouble p, AlignHorizontal align, double fontSize) {
ctxSetFont(fontSize, line);
String textToDraw = line.getStringWithoutMarkup();
if (textToDraw == null || textToDraw.isEmpty()) {
return; // if nothing should be drawn return (some browsers like Opera have problems with ctx.fillText calls on empty strings)
}
ctxSetTextAlign(align);
ctx.fillText(textToDraw, p.x, p.y);
if (line.getFormat().contains(FormatLabels.UNDERLINE)) {
ctx.setLineWidth(1.0f);
setLineDash(ctx, LineType.SOLID, 1.0f);
double textWidth = textWidth(line);
int vDist = 1;
switch (align) {
case LEFT:
drawLineHelper(true, new PointDouble(p.x, p.y + vDist), new PointDouble(p.x + textWidth, p.y + vDist));
break;
case CENTER:
drawLineHelper(true, new PointDouble(p.x - textWidth / 2, p.y + vDist), new PointDouble(p.x + textWidth / 2, p.y + vDist));
break;
case RIGHT:
drawLineHelper(true, new PointDouble(p.x - textWidth, p.y + vDist), new PointDouble(p.x, p.y + vDist));
break;
}
}
}
private void ctxSetFont(double fontSize, StringStyle stringStyle) {
String htmlStyle = "";
if (stringStyle.getFormat().contains(FormatLabels.BOLD)) {
htmlStyle += " bold";
}
if (stringStyle.getFormat().contains(FormatLabels.ITALIC)) {
htmlStyle += " italic";
}
ctx.setFont(htmlStyle + " " + fontSize + "px sans-serif");
}
private void ctxSetTextAlign(AlignHorizontal align) {
TextAlign ctxAlign = null;
switch (align) {
case LEFT:
ctxAlign = TextAlign.LEFT;
break;
case CENTER:
ctxAlign = TextAlign.CENTER;
break;
case RIGHT:
ctxAlign = TextAlign.RIGHT;
break;
}
ctx.setTextAlign(ctxAlign);
}
/**
* based on http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas/2173084#2173084
*/
private static void drawEllipseHelper(Context2d ctx, boolean drawOuterLine, double x, double y, double w, double h) {
double kappa = .5522848f;
double ox = w / 2 * kappa; // control point offset horizontal
double oy = h / 2 * kappa; // control point offset vertical
double xe = x + w; // x-end
double ye = y + h; // y-end
double xm = x + w / 2; // x-middle
double ym = y + h / 2; // y-middle
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.fill();
if (drawOuterLine) {
ctx.stroke();
}
}
/**
* based on http://js-bits.blogspot.co.at/2010/07/canvas-rounded-corner-rectangles.html
*/
private static void drawRoundRectHelper(Context2d ctx, boolean drawOuterLine, final double x, final double y, final double width, final double height, final double radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
if (drawOuterLine) {
ctx.stroke();
}
}
private void drawLineHelper(boolean drawOuterLine, PointDouble... points) {
ctx.beginPath();
boolean first = true;
for (PointDouble point : points) {
if (first) {
ctx.moveTo(point.x.intValue() + HALF_PX, point.y.intValue() + HALF_PX); // +0.5 because a line of thickness 1.0 spans 50% left and 50% right (therefore it would not be on the 1 pixel - see https://developer.mozilla.org/en-US/docs/HTML/Canvas/Tutorial/Applying_styles_and_colors)
first = false;
}
ctx.lineTo(point.x.intValue() + HALF_PX, point.y.intValue() + HALF_PX);
}
if (points[0].equals(points[points.length - 1])) {
ctx.fill(); // only fill if first point == lastpoint
}
if (drawOuterLine) {
ctx.stroke();
}
}
private void setStyle(Context2d ctx, Style style) {
if (style.getBackgroundColor() != null) {
ctx.setFillStyle(Converter.convert(style.getBackgroundColor()));
}
ColorOwn fgColor = getOverlay().getForegroundColor() != null ? getOverlay().getForegroundColor() : style.getForegroundColor();
if (fgColor != null) {
ctx.setStrokeStyle(Converter.convert(fgColor));
}
ctx.setLineWidth(style.getLineWidth());
setLineDash(ctx, style.getLineType(), style.getLineWidth());
}
private void setLineDash(Context2d ctx, LineType lineType, double lineThickness) {
try {
switch (lineType) {
case DASHED: // large linethickness values need longer dashes
setLineDash(ctx, 6 * Math.max(1, lineThickness / 2));
break;
case DOTTED: // minimum must be 2, otherwise the dotting is not really visible
setLineDash(ctx, Math.max(2, lineThickness));
break;
default: // default is a solid line
setLineDash(ctx, 0);
}
} catch (Exception e) {
log.debug("No browser support for dashed lines", e);
Notification.showFeatureNotSupported("Dashed and dotted lines are shown as solid lines<br/>To correctly display them, please use Firefox or Chrome", true);
}
}
/**
* Chrome and Firefox 33+ support setLineDash()
* Older Firefox version support only mozDash()
*/
public final native void setLineDash(Context2d ctx, double dash) /*-{
if (ctx.setLineDash !== undefined) {
if (dash != 0) {
ctx.setLineDash([ dash ]);
} else {
ctx.setLineDash([]); // Firefox 33+ on Linux dont show solid lines if ctx.setLineDash([0]) is used, therefore use empty array which works on every browser
}
} else if (ctx.mozDash !== undefined) {
if (dash != 0) {
ctx.mozDash = [ dash ];
} else { // default is null
ctx.mozDash = null;
}
} else if (dash != 0) { // if another line than a solid one should be set and the browser doesn't support it throw an Exception
throw new Exception();
}
}-*/;
}